{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "radio-chicken",
   "metadata": {},
   "source": [
    "> 初版，没有修正。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "moderate-florence",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 自动计算cell的计算时间\n",
    "%load_ext autotime\n",
    "\n",
    "#设置使用的gpu\n",
    "import tensorflow as tf\n",
    "\n",
    "gpus = tf.config.list_physical_devices(\"GPU\")\n",
    "\n",
    "if gpus:\n",
    "   \n",
    "    gpu0 = gpus[2] #如果有多个GPU，仅使用第0个GPU\n",
    "    tf.config.experimental.set_memory_growth(gpu0, True) #设置GPU显存用量按需使用\n",
    "    # 或者也可以设置GPU显存为固定使用量(例如：4G)\n",
    "    #tf.config.experimental.set_virtual_device_configuration(gpu0,\n",
    "    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)]) \n",
    "    tf.config.set_visible_devices([gpu0],\"GPU\") "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unavailable-polyester",
   "metadata": {},
   "source": [
    "# 神经网络的数学构建模块\n",
    "\n",
    "> 本章涵盖：\n",
    ">* 神经网络的第一个例子\n",
    ">* 张量和张量运算\n",
    ">* 神经网络如何通过反向传播和梯度下降进行学习\n",
    "\n",
    "理解深度学习需要熟悉很多简单的数学概念：张量张量运算微分梯度下降，，，，等等。 我们在本章中的目标是建立您对这些概念的直觉，而不会过于技术化。 特别是，我们将避开数学符号，它会给没有任何数学背景的人带来不必要的障碍，并且没有必要很好地解释事物。 对数学运算最准确、最明确的描述是它的可执行代码。\n",
    "\n",
    "为了提供足够的背景介绍张量和梯度下降，我们将从一个神经网络的实际例子开始本章。 然后我们将逐点回顾每个引入的新概念。 请记住，这些概念对于您理解后续章节中将出现的实际示例至关重要！\n",
    "\n",
    "阅读本章后，您将对深度学习背后的数学理论有一个直观的理解，并且您将准备好在第 3 章中开始深入研究 Keras 和 TensorFlow。\n",
    "\n",
    "## 神经网络初探\n",
    "\n",
    "让我们看一个使用 Python 库 Keras 学习对手写数字进行分类的神经网络的具体示例。除非您已经有使用 Keras 或类似库的经验，否则您不会立即了解第一个示例的所有内容。没关系。在下一章中，我们将回顾示例中的每个元素并详细解释它们。因此，如果某些步骤对您来说看起来很随意或看起来很神奇，请不要担心！我们必须从某个地方开始。\n",
    "\n",
    "我们在这里试图解决的问题是将手写数字（28 × 28 像素）的灰度图像分为 10 个类别（0 到 9）。我们将使用 MNIST 数据集，这是机器学习社区中的经典数据集，它的存在时间几乎与该领域本身一样长，并且已经过深入研究。这是一组 60,000 张训练图像和 10,000 张测试图像，由美国国家标准与技术研究所（MNIST 中的 NIST）在 1980 年代组装。您可以将“解决”MNIST 视为深度学习的“Hello World”——这是您验证算法是否按预期工作的方式。当您成为机器学习从业者时，您会看到 MNIST 一遍又一遍地出现在科学论文、博客文章等中。你可以在图 2.1 中看到一些 MNIST 样本。\n",
    "> 关于类别和标签的注意事项： 在机器学习中，分类问题中的种类称为类别，数据点称为样本。 与特定样本相关联的类是称为标签的样本。\n",
    "\n",
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsgzlf8ctjj31em0ag0tj.jpg)\n",
    "\n",
    "您现在不需要尝试在您的机器上重现此示例。 如果您愿意，您首先需要设置一个深度学习工作区，这将在第 3 章中介绍。\n",
    "\n",
    "MNIST 数据集以一组四个 NumPy 数组的形式预加载在 Keras 中。\n",
    "\n",
    "> 清单 2.1 在 Keras 中加载 MNIST 数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 91,
   "id": "naval-objective",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 681 ms (started: 2021-07-17 21:03:32 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.datasets import mnist\n",
    "\n",
    "(train_images, train_labels), (test_images, test_labels) = mnist.load_data()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "treated-challenge",
   "metadata": {},
   "source": [
    "`train_images` 和 `train_labels` 构成训练集，即模型将学习的数据。 然后将在测试集 `test_images` 和 `test_labels` 上测试模型。 图片被编码为NumPy数组，标签是一个数字数组，范围从0到9。图片和标签一一对应。\n",
    "\n",
    "让我们看一下训练数据："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "id": "received-murder",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(60000, 28, 28)"
      ]
     },
     "execution_count": 92,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.37 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_images.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 93,
   "id": "timely-glenn",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "60000"
      ]
     },
     "execution_count": 93,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.68 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "len(train_labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 94,
   "id": "warming-native",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)"
      ]
     },
     "execution_count": 94,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.98 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_labels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "another-cardiff",
   "metadata": {},
   "source": [
    "这是测试数据："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 95,
   "id": "random-testament",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(10000, 28, 28)"
      ]
     },
     "execution_count": 95,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.73 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "test_images.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 96,
   "id": "collective-iceland",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10000"
      ]
     },
     "execution_count": 96,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.52 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "len(test_images)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 97,
   "id": "standard-basis",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)"
      ]
     },
     "execution_count": 97,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.47 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "test_labels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "shaped-paradise",
   "metadata": {},
   "source": [
    "工作流程如下：首先，我们将向神经网络提供训练数据 `train_images`和`train_labels`。 然后网络将学习关联图像和标签。 最后，我们将要求网络为`test_images`生成预测，我们将验证这些预测是否与`test_labels`中的标签匹配。\n",
    "\n",
    "让我们构建网络 - 再次记住，您还不需要了解有关此示例的所有内容。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "regional-showcase",
   "metadata": {},
   "source": [
    "> 清单 2.2 网络架构"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 98,
   "id": "strategic-sherman",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.87 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow import keras\n",
    "from tensorflow.keras import layers\n",
    "model = keras.Sequential([\n",
    "    layers.Dense(512, activation=\"relu\"),\n",
    "    layers.Dense(10, activation=\"softmax\")\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "corrected-place",
   "metadata": {},
   "source": [
    "神经网络的核心构建块是 . 您可以将图层视为图层数据的过滤器：一些数据进入，它以更有用的形式出现。 具体来说，层从输入它们的数据中提取表示——希望这些表示对手头的问题更有意义。 大多数深度学习包括将简单的层链接在一起，这些层将实现一种渐进式数据提取形式。 深度学习模型就像数据处理的筛子，由一系列越来越精细的数据过滤器（层）组成。\n",
    "\n",
    "在这里，我们的模型由两层的序列组成，它们是密集连接（也称为密集连接）的神经层。 第二层（也是最后一层）是一个 10 路 softmax 分类层，这意味着它将返回一个包含 10 个概率分数（总和为 1）的数组。 每个分数将是当前数字图像属于我们的 10 个数字类别之一的概率。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "revised-bedroom",
   "metadata": {},
   "source": [
    "为了让模型为训练做好准备，我们还需要选择三件事，作为编译步骤的一部分：\n",
    "* 优化器——模型将根据它看到的训练数据更新自身以提高其性能的机制。\n",
    "* 损失函数——模型如何能够衡量其在训练数据上的表现，从而能够将自己引导到正确的方向。\n",
    "* 在训练和测试期间监控的指标——在这里，我们只关心准确性（正确分类的图像的比例）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "northern-monitoring",
   "metadata": {},
   "source": [
    "损失函数和优化器的确切目的将在接下来的两章中阐明。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "circular-exhibition",
   "metadata": {},
   "source": [
    "> 清单 2.3 编译步骤"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 99,
   "id": "phantom-renewal",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.93 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.compile(optimizer=\"rmsprop\",\n",
    "              loss=\"sparse_categorical_crossentropy\",\n",
    "              metrics=[\"accuracy\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "looking-worship",
   "metadata": {},
   "source": [
    "在训练之前，我们将通过将数据重塑为模型期望的形状并对其进行缩放来预处理数据，以便所有值都在区间内。 以前，我们的训练图像 [0, 1] 存储在类型为 (60000, 28, 28) uint8 的数组中，其值在 [0, 255] 区间内。 我们将其转换为一个形状为 float32 (60000, 28 * 28) 的数组，其值介于 0 和 1 之间。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "standard-element",
   "metadata": {},
   "source": [
    "> 清单 2.4 准备图像数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 100,
   "id": "moderate-advertiser",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 75.9 ms (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_images = train_images.reshape((60000, 28 * 28))\n",
    "train_images = train_images.astype(\"float32\") / 255\n",
    "test_images = test_images.reshape((10000, 28 * 28))\n",
    "test_images = test_images.astype(\"float32\") / 255"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "grateful-uncle",
   "metadata": {},
   "source": [
    "我们现在准备训练模型，在 Keras 中是通过调用模型的 fit() 方法来完成的——我们将模型训练到它的训练数据："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "superb-membership",
   "metadata": {},
   "source": [
    "> 清单 2.5 “拟合”模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 101,
   "id": "suspected-legislature",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5\n",
      "375/375 [==============================] - 3s 7ms/step - loss: 0.2896 - accuracy: 0.9172 - val_loss: 0.1522 - val_accuracy: 0.9576\n",
      "Epoch 2/5\n",
      "375/375 [==============================] - 3s 7ms/step - loss: 0.1199 - accuracy: 0.9644 - val_loss: 0.1085 - val_accuracy: 0.9678\n",
      "Epoch 3/5\n",
      "375/375 [==============================] - 2s 7ms/step - loss: 0.0785 - accuracy: 0.9769 - val_loss: 0.1009 - val_accuracy: 0.9701\n",
      "Epoch 4/5\n",
      "375/375 [==============================] - 2s 7ms/step - loss: 0.0560 - accuracy: 0.9836 - val_loss: 0.1009 - val_accuracy: 0.9722\n",
      "Epoch 5/5\n",
      "375/375 [==============================] - 2s 7ms/step - loss: 0.0420 - accuracy: 0.9877 - val_loss: 0.0881 - val_accuracy: 0.9768\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<tensorflow.python.keras.callbacks.History at 0x7fb9d8291670>"
      ]
     },
     "execution_count": 101,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 13 s (started: 2021-07-17 21:03:33 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.fit(train_images, train_labels, epochs=5, batch_size=128, validation_split=0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "meaningful-basement",
   "metadata": {},
   "source": [
    "训练期间会显示两个量：模型在训练数据上的损失，以及模型在训练数据上的准确度。 我们很快在训练数据上达到了 0.989 (98.9%) 的准确率。\n",
    "\n",
    "现在我们有了一个经过训练的模型，您可以使用它来预测新数字的类别概率 - 不属于训练数据的图像，例如来自测试集的图像："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "wound-discipline",
   "metadata": {},
   "source": [
    "> 清单 2.6 使用模型进行预测"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 102,
   "id": "corresponding-possibility",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1.7760838e-09, 8.6447842e-11, 2.1288886e-06, 1.7542792e-05,\n",
       "       7.0246678e-13, 1.1248878e-08, 7.4850221e-14, 9.9997926e-01,\n",
       "       1.4568435e-08, 1.0932807e-06], dtype=float32)"
      ]
     },
     "execution_count": 102,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 66.4 ms (started: 2021-07-17 21:03:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "test_digits = test_images[0:10]\n",
    "predictions = model.predict(test_digits)\n",
    "predictions[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "liable-comparison",
   "metadata": {},
   "source": [
    "该数组中的每个索引数i对应于数字图像test_digits[0]属于 class 的概率。\n",
    "\n",
    "第一个测试数字在索引 7 处的概率得分最高（0.99999106，几乎为 1），因此根据我们的模型，它必须是 7："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 103,
   "id": "plastic-width",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "7"
      ]
     },
     "execution_count": 103,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.6 ms (started: 2021-07-17 21:03:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "predictions[0].argmax()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 104,
   "id": "magnetic-evolution",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.99997926"
      ]
     },
     "execution_count": 104,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.07 ms (started: 2021-07-17 21:03:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "predictions[0][7]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "expressed-complaint",
   "metadata": {},
   "source": [
    "我们可以检查测试标签是否一致："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 105,
   "id": "increased-audio",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "7"
      ]
     },
     "execution_count": 105,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 12.6 ms (started: 2021-07-17 21:03:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "test_labels[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "limiting-title",
   "metadata": {},
   "source": [
    "平均而言，我们的模型在对这些前所未见的数字进行分类方面有多好？ 让我们通过计算整个测试集的平均准确度来检查。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "southeast-empty",
   "metadata": {},
   "source": [
    "> 清单 2.7 在新数据上评估模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 106,
   "id": "false-suffering",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "313/313 [==============================] - 2s 7ms/step - loss: 0.0748 - accuracy: 0.9777\n",
      "time: 2.35 s (started: 2021-07-17 21:03:46 +08:00)\n"
     ]
    }
   ],
   "source": [
    "test_loss, test_acc = model.evaluate(test_images, test_labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "id": "quality-injury",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test_acc: 0.9776999950408936\n",
      "time: 313 µs (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "print(f\"test_acc: {test_acc}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exempt-internet",
   "metadata": {},
   "source": [
    "结果证明测试集的准确率为 97.8%——这比训练集的准确率 (98.9%) 低很多。 训练准确度和测试准确度之间的这种差距就是过度拟合的一个例子：机器学习模型在新数据上的表现往往比在训练数据上差。 过拟合是第 3 章的中心主题。\n",
    "\n",
    "我们的第一个示例到此结束——您刚刚看到了如何构建和训练神经网络，用不到 15 行的 Python 代码对手写数字进行分类。 在本章和下一章中，我们将详细介绍我们刚刚预览的每个动人片段，并阐明幕后发生的事情。 您将了解张量，即进入模型的数据存储对象； 张量操作，由哪些层组成； 和梯度下降，它允许您的模型从其训练示例中学习。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hearing-dodge",
   "metadata": {},
   "source": [
    "## 神经网络的数据表示"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "equivalent-motor",
   "metadata": {},
   "source": [
    "在前面的示例中，我们从存储在多维 NumPy 数组（也称为张量)， 一般来说，当前所有的机器学习系统都使用张量作为它们的基本数据张量结构体。 张量是该领域的基础——如此重要以至于 TensorFlow 以它们命名。 那么什么是张量？\n",
    "\n",
    "从本质上讲，张量是数据的容器——通常是数值数据。 所以，它是数字的容器。 您可能已经熟悉矩阵，它们是 2 阶张量：张量是矩阵到任意数量的泛化（请注意，在维度张量的上下文中，维度通常称为 `axis` ）。 "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "interpreted-determination",
   "metadata": {},
   "source": [
    "### 标量（0阶张量）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "printable-character",
   "metadata": {},
   "source": [
    "仅包含一个数字的张量称为标量（或标量张量，或 0 阶张量，或标量 0D 张量）。 在 NumPy 中， float32或者floa64（或标量数组）是一个标量。 可以通过ndim属性显示 NumPy 张量的轴数； 标量张量有 0 个轴 (ndim==0 )。 张量的轴数也称为它的秩。 这是一个 NumPy 标量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 108,
   "id": "sensitive-knock",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.77 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 109,
   "id": "north-product",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array(12)"
      ]
     },
     "execution_count": 109,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.73 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.array(12)\n",
    "x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 110,
   "id": "centered-cleaners",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 110,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.18 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "balanced-quick",
   "metadata": {},
   "source": [
    "### 向量（1阶张量）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "placed-tragedy",
   "metadata": {},
   "source": [
    "数字数组称为向量，或 rank-1 张量或 1D 张量。 1 阶张量只有一个轴。 以下是一个 NumPy 向量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 111,
   "id": "partial-syntax",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 604 µs (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.array([12, 3, 6, 14, 7])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 112,
   "id": "radio-cambodia",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([12,  3,  6, 14,  7])"
      ]
     },
     "execution_count": 112,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.57 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 113,
   "id": "refined-canyon",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 113,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.32 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "obvious-colleague",
   "metadata": {},
   "source": [
    "该向量有五个条目，因此称为 5 维向量。 不要将 5D 矢量与 5D 张量混淆！ 一个 5D 向量只有一个轴并且沿着它的轴有五个维度，而一个 5D 张量有五个轴（并且沿着每个轴可以有任意数量的维度）。 维度可以表示沿特定轴的条目数（如我们的 5D 向量的情况）或张量中的轴数（如 5D 张量），这有时会令人困惑。 在后一种情况下，谈论 5 阶张量（张量的阶是轴的数量）在技术上更正确，但无论如何，模棱两可的表示法很常见。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "arctic-harris",
   "metadata": {},
   "source": [
    "### 矩阵（二阶张量）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "prostate-sampling",
   "metadata": {},
   "source": [
    "向量数组是一个 ，或 rank-2 张量，或 2D 张量。 矩阵有两个轴（通常称为 和 的矩阵）。 您可以直观地将矩阵解释为行列编号的矩形网格。 这是一个 NumPy 矩阵："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 114,
   "id": "amateur-builder",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 756 µs (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.array([[5, 78, 2, 34, 0],\n",
    "              [6, 79, 3, 35, 1],\n",
    "              [7, 80, 4, 36, 2]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 115,
   "id": "veterinary-ending",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 115,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.33 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fluid-lease",
   "metadata": {},
   "source": [
    "来自第一个轴的条目称为行，来自第二个轴的条目称为列。 在前面的示例中，[5, 78, 2, 34, 0] 是 x 的第一行，而 [5, 6, 7] 是第一列。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "engaged-patent",
   "metadata": {},
   "source": [
    "### 3阶张量和更高阶张量"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "composite-jersey",
   "metadata": {},
   "source": [
    "如果您将这些矩阵打包到一个新数组中，您将获得一个 3 阶张量（或 3D 张量），您可以直观地将其解释为数字的立方体。 以下是 NumPy 3阶张量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 116,
   "id": "steady-booth",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.05 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    " x = np.array([[[5, 78, 2, 34, 0],\n",
    "                [6, 79, 3, 35, 1],\n",
    "                [7, 80, 4, 36, 2]],\n",
    "               [[5, 78, 2, 34, 0],\n",
    "                [6, 79, 3, 35, 1],\n",
    "                [7, 80, 4, 36, 2]],\n",
    "               [[5, 78, 2, 34, 0],\n",
    "                [6, 79, 3, 35, 1],\n",
    "                [7, 80, 4, 36, 2]]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 117,
   "id": "sudden-words",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 117,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.77 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "contrary-grill",
   "metadata": {},
   "source": [
    "通过将3阶级张量打包在一个数组中，您可以创建一个4阶级张量，依此类推。 在深度学习中，您通常会操作 0 到 4阶的张量，但如果您处理视频数据，则可能会达到5。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "major-franchise",
   "metadata": {},
   "source": [
    "### 关键属性"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sustainable-plate",
   "metadata": {},
   "source": [
    "张量由三个关键属性定义：\n",
    "\n",
    "* 轴数（秩）——例如，秩为 3 的张量具有三个轴，而矩阵具有两个轴。 这在 Python 库（例如NumPy或TensorFlow）中也称为张量的ndim。\n",
    "* 形状——这是一个整数元组，描述张量沿每个轴有多少维。 例如，前面的矩阵示例具有 shape ，而 (3, 5) 秩 3 张量示例具有 shape。 向量具有单个元素 (3, 3, 5) 的形状，例如 ，而标量具有空形状（）。\n",
    "* 数据类型（通常在 Python 库中调用）——这是张量中包含的数据 dtype 的类型； 例如，张量的类型可以是 float16、float32、float32、float64、uint8 等。 在 TensorFlow 中，你也可能会遇到字符串张量"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "persistent-fever",
   "metadata": {},
   "source": [
    "为了更具体，让我们回顾一下我们在 MNIST 示例中处理的数据。 首先，我们加载 MNIST 数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 118,
   "id": "painted-energy",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 276 ms (started: 2021-07-17 21:03:48 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.datasets import mnist\n",
    "(train_images, train_labels), (test_images, test_labels) = mnist.load_data()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "greater-netscape",
   "metadata": {},
   "source": [
    "接下来，我们显示张量 train_images ndim 的轴数，属性："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 119,
   "id": "incorrect-television",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 119,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.19 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_images.ndim"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "transparent-sharing",
   "metadata": {},
   "source": [
    "这是它的形状："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 120,
   "id": "blank-castle",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(60000, 28, 28)"
      ]
     },
     "execution_count": 120,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.09 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_images.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "signed-pressing",
   "metadata": {},
   "source": [
    "这是它的数据类型，属性：dtype"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 121,
   "id": "existing-riding",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dtype('uint8')"
      ]
     },
     "execution_count": 121,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.3 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_images.dtype"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "wired-sixth",
   "metadata": {},
   "source": [
    "所以我们这里有一个 3 级的 8 位整数张量。 更准确地说，它是一个由 60,000 个 28 × 28 整数矩阵组成的数组。 每个这样的矩阵都是一个灰度图像，系数在 0 到 255 之间。\n",
    "\n",
    "让我们使用库 Matplotlib（标准科学 Python 套件的一部分）显示这个 3 级张量中的第四个数字； 见图 2.2。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "assured-title",
   "metadata": {},
   "source": [
    "> 清单 2.8 显示第四位数字"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 122,
   "id": "suited-interest",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 118 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "digit = train_images[4]\n",
    "plt.imshow(digit, cmap=plt.cm.binary)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "funky-lingerie",
   "metadata": {},
   "source": [
    "自然，对应的标签就是整数 9："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 123,
   "id": "computational-yellow",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9"
      ]
     },
     "execution_count": 123,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.16 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "train_labels[4]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "becoming-obligation",
   "metadata": {},
   "source": [
    "### 在 NumPy 中操作张量"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "legitimate-marsh",
   "metadata": {},
   "source": [
    "在前面的示例中，我们使用语法 train_images[i] 选择了第一个轴旁边的特定数字。 选择张量中的特定元素称为张量切片。 让我们看看您可以在 NumPy 数组上执行的张量切片操作。\n",
    "\n",
    "以下示例选择数字 #10 到 #100（不包括 #100）并将它们放入形状为 (90, 28, 28) 的数组中："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 124,
   "id": "killing-bahamas",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 755 ms (started: 2021-07-17 21:03:49 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice = train_images[10:100]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 125,
   "id": "supposed-directory",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(90, 28, 28)"
      ]
     },
     "execution_count": 125,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 928 ms (started: 2021-07-17 21:03:50 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spiritual-outdoors",
   "metadata": {},
   "source": [
    "它相当于这个更详细的符号，它指定了沿着每个张量轴的切片的开始索引和停止索引。 请注意，这相当于选择整个轴："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 126,
   "id": "authentic-empire",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 726 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice = train_images[10:100, :, :]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 127,
   "id": "passing-professor",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(90, 28, 28)"
      ]
     },
     "execution_count": 127,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.43 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 128,
   "id": "alpine-jason",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 502 µs (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice = train_images[10:100, 0:28, 0:28]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 129,
   "id": "isolated-level",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(90, 28, 28)"
      ]
     },
     "execution_count": 129,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.17 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "distinguished-opening",
   "metadata": {},
   "source": [
    "通常，您可以选择沿每个张量轴的任意两个索引之间的切片。 例如，为了在所有图像的右下角选择 14 × 14 像素，您可以这样做："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 130,
   "id": "agreed-cancer",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 447 µs (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice = train_images[:, 14:, 14:]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "communist-mileage",
   "metadata": {},
   "source": [
    "也可以使用负索引。 就像 Python 列表中的负索引一样，它们表示相对于当前轴末端的位置。 为了将图像裁剪为以中间为中心的 14 × 14 像素块，您可以执行以下操作："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 131,
   "id": "empty-demand",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 721 µs (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "my_slice = train_images[:, 7:-7, 7:-7]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "nearby-bloom",
   "metadata": {},
   "source": [
    "### 数据批次的概念"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rising-stewart",
   "metadata": {},
   "source": [
    "通常，您在深度学习中遇到的所有数据张量中的第一个轴（轴 0，因为索引从 0 开始）将是样本轴（有时称为样本维度）。 在 MNIST 示例中，“样本”是数字图像。\n",
    "\n",
    "此外，深度学习模型不会一次处理整个数据集； 相反，他们将数据分成小批量。 具体来说，这是我们的一批 MNIST 数字，批大小为 128："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 132,
   "id": "absent-reception",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 695 µs (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "batch = train_images[:128]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "norman-gabriel",
   "metadata": {},
   "source": [
    "这是下一批："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 133,
   "id": "chicken-biotechnology",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.31 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "batch = train_images[128:256]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "juvenile-wichita",
   "metadata": {},
   "source": [
    "第n批:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 134,
   "id": "administrative-auckland",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.02 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "n = 3\n",
    "batch = train_images[128 * n:128 * (n + 1)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "according-arizona",
   "metadata": {},
   "source": [
    "在考虑这样的批次张量时，第一个轴（轴 0）称为批次轴批次维度。 这是您在使用 Keras 和其他深度学习库时会经常遇到的术语。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "coupled-group",
   "metadata": {},
   "source": [
    "### 数据张量的真实例子"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "increased-template",
   "metadata": {},
   "source": [
    "让我们通过一些类似于您稍后将遇到的示例来使数据张量更加具体。 您将操作的数据几乎总是属于以下类别之一：\n",
    "* 矢量数据 - 2阶形状张量（样本、特征），其中每个样本是数值属性（“特征”）的向量。\n",
    "* 时间序列数据或序列数据 — 3阶形状张量（样本、时间步长、特征），其中每个样本是一个（长度为 ）特征向量的序列。\n",
    "* 图像 - 4阶级形状张量（样本、高度、宽度、通道），其中每个样本是一个 2D 像素网格，每个像素由值向量（“通道”）表示。\n",
    "* 视频 - 5阶形状张量（样本、帧、高度、宽度、通道），其中每个样本是一个（长度帧）图像序列"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "graphic-filing",
   "metadata": {},
   "source": [
    "### 矢量数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "recovered-shanghai",
   "metadata": {},
   "source": [
    "这是最常见的情况之一。 在这样的数据集中，每个单个数据点都可以编码为一个向量，因此一批数据将被编码为一个秩为 2 的张量（即向量数组），其中第一个轴是样本轴， 第二个轴是特征轴。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "written-drove",
   "metadata": {},
   "source": [
    "我们来看两个例子：\n",
    "* 一个人的精算数据集，我们在其中考虑每个人的年龄、性别和收入。 每个人都可以表征为一个包含 3 个值的向量，因此 100,000 人的整个数据集可以存储在形状为 (100000, 3) 的 2 级张量中。\n",
    "* 文本文档的数据集，我们通过每个单词出现在其中的次数来表示每个文档（在 20,000 个常用单词的字典中）。 每个文档都可以编码为一个包含 20,000 个值的向量（字典中每个单词一个计数），因此可以将包含 500 个文档的整个数据集存储在形状为 (500, 20000) 的张量中。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "educated-apache",
   "metadata": {},
   "source": [
    "### 时间序列数据或序列数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dependent-magnitude",
   "metadata": {},
   "source": [
    "每当您的数据（或序列顺序的概念）中的时间很重要时，将其存储在具有明确时间轴的 3 级张量中是有意义的。 每个样本都可以编码为一个向量序列（一个 rank-2 张量），因此一批数据将被编码为一个 rank-3 张量（见图 2.3）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "mineral-adobe",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNly1gsjxu1s347j31fy0lstaj.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "plain-welcome",
   "metadata": {},
   "source": [
    "按照惯例，时间轴始终是第二个轴（索引 1 的轴）。 让我们看几个例子：\n",
    "* 股票价格数据集。 每分钟，我们存储股票的当前价格，过去一分钟的最高价格，以及过去一分钟的最低价格。 因此，每一分钟都被编码为一个 3D 向量，一整天的交易被编码为一个形状为 (390, 3) 的矩阵（一个交易日有 390 分钟），250 天的数据可以存储在一个 3 级形状张量 (250, 390, 3)。 在这里，每个样本都是一天的数据。\n",
    "* 推文数据集，我们将每条推文编码为 128 个唯一字符字母表中的 280 个字符序列。 在此设置中，每个字符都可以编码为大小为 128 的二进制向量（除了与字符对应的索引处的 1 条目之外的全零向量）。 然后可以将每条推文编码为形状为 (280, 128) 的 2 级张量，并且可以将 100 万条推文的数据集存储在形状为 (1000000, 280, 128) 的张量中。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "swiss-raising",
   "metadata": {},
   "source": [
    "### 图像数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hollywood-satisfaction",
   "metadata": {},
   "source": [
    "图像通常具有三个维度：高度、宽度和颜色深度。 尽管灰度图像（如我们的 MNIST 数字）只有一个颜色通道，因此可以存储在 2 阶张量中，但按照惯例，图像张量始终是 3 阶，灰度图像具有一维颜色通道。 因此，一批 128 张大小为 256 × 256 的灰度图像可以存储在形状为 (128, 256, 256, 1) 的张量中，一批 128 张彩色图像可以存储在形状为 (128, 256, 256, 3)（见图 2.4）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rapid-abraham",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNly1gsjy2mn11nj30p80iyab6.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "searching-sampling",
   "metadata": {},
   "source": [
    "图像张量的形状有两种约定：通道最后约定（TensorFlow 中的标准）和通道优先约定（越来越不受欢迎）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rapid-royalty",
   "metadata": {},
   "source": [
    "通道最后约定将颜色深度轴放在末尾：（样本、高度、宽度、颜色深度）。 同时，通道优先约定将颜色深度轴放在批处理轴之后：（样本，颜色深度，高度，宽度）。 使用通道优先约定，前面的示例将变为 (128, 1, 256, 256) (128, 3, 256, and 256)。 Keras API 提供对这两种格式的支持。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "twelve-prescription",
   "metadata": {},
   "source": [
    "### 视频数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "crucial-edition",
   "metadata": {},
   "source": [
    "视频数据是少数需要 5 级张量的真实世界数据类型之一。 视频可以理解为一系列帧，每一帧都是一张彩色图像。 因为每一帧都可以存储在一个rank-3张量（height, width, color_depth）中，一个帧序列可以存储在一个rank-4张量（frames, height, width, color_depth）中，从而得到一批不同的视频 可以存储在 5 级形状张量中（样本、帧、高度、宽度、颜色深度）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "crude-count",
   "metadata": {},
   "source": [
    "例如，以每秒 4 帧采样的 60 秒、144 × 256 YouTube 视频剪辑将有 240 帧。 一批四个这样的视频剪辑将存储在一个形状为 (4, 240, 144, 256, 3) 的张量中。 总共有 106,168,320 个值！ 如果张量的 , dtype float32 那么每个值将以 32 位存储，因此张量将表示 405 MB。 重的！ 您在现实生活中遇到的视频要轻得多，因为它们没有存储在 .float32 和 float32 中，它们通常被大量压缩（例如以 MPEG 格式）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "moved-protest",
   "metadata": {},
   "source": [
    "## 神经网络的齿轮：张量运算 "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "public-french",
   "metadata": {},
   "source": [
    "就像任何计算机程序最终都可以简化为对二进制输入（AND、OR、NOR 等）进行的一小组二进制运算一样，深度神经网络学习的所有变换都可以简化为少量张量运算（ 或 )张量函数应用于数值数据的张量。 例如，可以添加张量、乘张量等。\n",
    "\n",
    "在我们最初的示例中，我们通过在每个 Dense 彼此之上堆叠层来构建我们的模型。 Keras 层实例如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 135,
   "id": "eligible-strap",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tensorflow.python.keras.layers.core.Dense at 0x7fb9d8464f10>"
      ]
     },
     "execution_count": 135,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.47 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "keras.layers.Dense(512, activation=\"relu\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "monetary-twins",
   "metadata": {},
   "source": [
    "这一层可以被解释为一个函数，它将一个矩阵作为输入并返回另一个矩阵——输入张量的新表示。 具体来说，函数如下（其中W是矩阵，b是向量，都是层的属性）：\n",
    "```python\n",
    "output = relu(dot(input, W) + b)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "about-huntington",
   "metadata": {},
   "source": [
    "让我们解开这个。 我们这里有三个张量操作：\n",
    "* 输入张量和名为点 W 的张量之间的点积 ( )\n",
    "* 结果矩阵和向量 b + 之间的加法 ( )\n",
    "* Relu 操作：是max(x,0). “relu”代表“整流线性单元”。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unavailable-worship",
   "metadata": {},
   "source": [
    "> 尽管本节完全涉及线性代数表达式，但您不会在这里找到任何数学符号。 我发现没有数学背景的程序员可以更容易地掌握数学概念，如果将它们表示为简短的 Python 片段而不是数学方程。 所以我们将在整个过程中使用 NumPy 和 TensorFlow 代码。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "twelve-professional",
   "metadata": {},
   "source": [
    "### 元素操作"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "increasing-surface",
   "metadata": {},
   "source": [
    "操作和加法是逐元素操作：将 relu 独立应用于所考虑的张量中的每个条目的操作。 这意味着这些操作非常适合大规模并行实现（实现，向量化的术语来自 1970-1990 时期的向量处理器超级计算机架构）。 如果你想编写一个简单的元素操作的 Python 实现，你可以使用一个循环，因为在这个元素操作的简单实现中：relu\n",
    "```python\n",
    "def naive_relu(x):\n",
    "    assert len(x.shape) == 2\n",
    "    x = x.copy()\n",
    "    for i in range(x.shape[0]):\n",
    "        for j in range(x.shape[1]):\n",
    "            x[i, j] = max(x[i, j], 0)\n",
    " return x\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "forbidden-heritage",
   "metadata": {},
   "source": [
    "你可以做同样的add函数：\n",
    "```python\n",
    "def naive_add(x, y):\n",
    "    assert len(x.shape) == 2\n",
    "    assert x.shape == y.shape\n",
    "    x = x.copy()\n",
    "    for i in range(x.shape[0]):\n",
    "        for j in range(x.shape[1]):\n",
    "            x[i, j] += y[i, j]\n",
    " return x\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fleet-dietary",
   "metadata": {},
   "source": [
    "根据相同的原理，您可以进行逐元素乘法、减法等。\n",
    "\n",
    "在实践中，在处理 NumPy 数组时，这些操作可以作为优化良好的内置 NumPy 函数使用，它们本身将繁重的工作委托给基本线性代数\n",
    "子程序 (BLAS) 实施。 BLAS 是低级、高度并行、高效的张量操作例程，通常在 Fortran 或 C 中实现。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "entitled-apparel",
   "metadata": {},
   "source": [
    "因此，在 NumPy 中，您可以执行以下逐元素操作，而且速度会非常快：\n",
    "```python\n",
    "import numpy as np\n",
    "z = x + y\n",
    "z = np.maximum(z, 0.)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cubic-class",
   "metadata": {},
   "source": [
    "让我们实际计算一下差异："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 136,
   "id": "martial-ukraine",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Took: 0.01 s\n",
      "time: 6.57 ms (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "x = np.random.random((20, 100))\n",
    "y = np.random.random((20, 100))\n",
    "t0 = time.time()\n",
    "for _ in range(1000):\n",
    "    z = x + y\n",
    "    z = np.maximum(z, 0.)\n",
    "    \n",
    "print(\"Took: {0:.2f} s\".format(time.time() - t0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "neural-adaptation",
   "metadata": {},
   "source": [
    "这需要 0.02 秒。 同时，天真的版本需要惊人的 2.45 秒："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 137,
   "id": "third-christmas",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.03 ms ± 20.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n",
      "time: 8.58 s (started: 2021-07-17 21:03:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "%timeit naive_relu(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lesbian-trainer",
   "metadata": {},
   "source": [
    "同样，当在 GPU 上运行 TensorFlow 代码时，元素操作是通过完全矢量化的 CUDA 实现来执行的，可以最好地利用高度并行的 GPU 芯片架构。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "considerable-bulgarian",
   "metadata": {},
   "source": [
    "### 广播"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "explicit-ceramic",
   "metadata": {},
   "source": [
    "我们早期的 naive 实现仅支持添加具有 naive_add 相同形状的 rank-2 张量。 但是在前面介绍的层中，我们添加了一个带有 Dense 向量的 rank-2 张量。 当相加的两个张量的形状不同时，加法会发生什么？\n",
    "\n",
    "在可能的情况下，如果没有歧义，将广播较小的张量以匹配较大张量的形状。 广播包括两个步骤：\n",
    "* 轴（称为广播轴）被添加到较小的张量以匹配 ndim 较大的张量。\n",
    "* 较小的张量与这些新轴一起重复，以匹配较大张量的完整形状。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pending-transcription",
   "metadata": {},
   "source": [
    "我们来看一个具体的例子。 考虑形状和形状：X (32, 10) y (10,)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 138,
   "id": "refined-slovenia",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 517 µs (started: 2021-07-17 21:04:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "X = np.random.random((32, 10))\n",
    "y = np.random.random((10,))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "neutral-farming",
   "metadata": {},
   "source": [
    "首先，我们向 中添加一个空的第一个轴，其形状变为：y (1, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "id": "altered-giving",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 10)"
      ]
     },
     "execution_count": 139,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.66 ms (started: 2021-07-17 21:04:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "y = np.expand_dims(y, axis=0)\n",
    "y.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "id": "equivalent-insert",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 1, 10)"
      ]
     },
     "execution_count": 140,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.35 ms (started: 2021-07-17 21:04:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "#或者采用这种形式，利用了python set的特性：\n",
    "y.reshape((1,)+y.shape).shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "wooden-color",
   "metadata": {},
   "source": [
    "然后，我们沿着这个新轴重复 32 次，这样我们最终会得到一个形状为 y Y (32, 10) Y[i, :] == y i range(0, 32) 的张量，其中 in ："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 141,
   "id": "unique-catholic",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 795 µs (started: 2021-07-17 21:05:43 +08:00)\n"
     ]
    }
   ],
   "source": [
    "Y = np.concatenate([y] * 32, axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "consecutive-lincoln",
   "metadata": {},
   "source": [
    "此时，我们可以继续添加X和Y，因为它们具有相同的形状\n",
    "\n",
    "在实现方面，没有创建新的2阶张量，因为那会非常低效。 重复操作完全是虚拟的：它发生在算法级别而不是内存级别。 但是，将向量与新轴一起重复 10 次的想法是一种有用的心理模型。 下面是一个简单的实现："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 143,
   "id": "departmental-columbus",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 598 µs (started: 2021-07-17 21:31:38 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def naive_add_matrix_and_vector(x, y):\n",
    "    assert len(x.shape) == 2\n",
    "    assert len(y.shape) == 1\n",
    "    assert x.shape[1] == y.shape[0]\n",
    "    x = x.copy()\n",
    "    for i in range(x.shape[0]):\n",
    "        for j in range(x.shape[1]):\n",
    "            x[i, j] += y[j]\n",
    "    return x"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hazardous-joseph",
   "metadata": {},
   "source": [
    "对于广播，如果一个张量具有形状 (a, b, … n, n + 1, … m) 而另一个具有形状 (n, n + 1, … m)，则通常可以应用双张量元素操作。 然后广播将自动发生在轴 a 到 n - 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "limiting-beast",
   "metadata": {},
   "source": [
    "以下示例通过广播将逐元素操作应用于两个不同最大形状的张量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 144,
   "id": "written-hampton",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.32 ms (started: 2021-07-17 21:33:28 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "x = np.random.random((64, 3, 32, 10))\n",
    "y = np.random.random((32, 10))\n",
    "z = np.maximum(x, y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "professional-friendship",
   "metadata": {},
   "source": [
    "### 张量积"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "boring-departure",
   "metadata": {},
   "source": [
    "张量积点积或（不要与元素乘积混淆， * 运算符）是最常见、最有用的张量运算之一。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "homeless-volunteer",
   "metadata": {},
   "source": [
    "在 NumPy 中，张量积是使用该函数完成的（因为张量积的数学 np.dot 表示法通常是一个点）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 146,
   "id": "interesting-blend",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9.37882579185326"
      ]
     },
     "execution_count": 146,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.2 ms (started: 2021-07-17 21:35:43 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.random.random((32,))\n",
    "y = np.random.random((32,))\n",
    "z = np.dot(x, y)\n",
    "z"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 148,
   "id": "virtual-reasoning",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9.37882579185326"
      ]
     },
     "execution_count": 148,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.97 ms (started: 2021-07-17 21:35:59 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x@y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amended-banks",
   "metadata": {},
   "source": [
    "在数学符号中，您会用一个点 (•) 来表示操作："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "adolescent-cocktail",
   "metadata": {},
   "outputs": [],
   "source": [
    "z = x • y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "genetic-princeton",
   "metadata": {},
   "source": [
    "在数学上，点操作有什么作用？ 让我们从两个向量 x 和 y的点积开始。 计算如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 149,
   "id": "filled-mixer",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 674 µs (started: 2021-07-17 21:37:28 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def naive_vector_dot(x, y):\n",
    "    assert len(x.shape) == 1\n",
    "    assert len(y.shape) == 1\n",
    "    assert x.shape[0] == y.shape[0]\n",
    "    z = 0.\n",
    "    for i in range(x.shape[0]):\n",
    "        z += x[i] * y[i]\n",
    "    return z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "derived-calcium",
   "metadata": {},
   "source": [
    "您会注意到两个向量之间的点积是一个标量，并且只有具有相同元素数量的向量才与点积兼容。\n",
    "\n",
    "您还可以采用矩阵X和向量y之间的点积，它返回向量 ，其中系数是 和 的行之间的点积。 其实现为\n",
    "如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 150,
   "id": "medium-scope",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 529 µs (started: 2021-07-17 21:41:22 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def naive_matrix_vector_dot(x, y):\n",
    "    assert len(x.shape) == 2\n",
    "    assert len(y.shape) == 1\n",
    "    assert x.shape[1] == y.shape[0]\n",
    "    z = np.zeros(x.shape[0])\n",
    "    for i in range(x.shape[0]):\n",
    "        for j in range(x.shape[1]):\n",
    "            z[i] += x[i, j] * y[j]\n",
    "    return z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "relevant-throat",
   "metadata": {},
   "source": [
    "您还可以重用我们之前编写的代码，其中突出显示了矩阵向量乘积和向量乘积之间的关系："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 152,
   "id": "worldwide-radio",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 693 µs (started: 2021-07-17 21:42:41 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def naive_matrix_vector_dot(x, y):\n",
    "    z = np.zeros(x.shape[0])\n",
    "    for i in range(x.shape[0]):\n",
    "        z[i] = naive_vector_dot(x[i, :], y)\n",
    "    return z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "derived-wallpaper",
   "metadata": {},
   "source": [
    "请注意，只要两个张量之一大于 1，就不再是 ndim 点对称，也就是说，与 不同。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "prescription-winner",
   "metadata": {},
   "source": [
    "当然，点积可以推广到具有任意数量轴的张量。 最常见的应用可能是两个矩阵之间的点积。 您可以取两个矩阵的点积和 ( ) 当且仅当 x y dot(x, y) x.shape[1] == y.shape[0]。 结果是一个形状为 (x.shape[0], y.shape[1]) 的矩阵，其中系数是 的行和列之间的向量积。 这是简单的实现："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 155,
   "id": "transparent-comparative",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 858 µs (started: 2021-07-17 21:44:56 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def naive_matrix_dot(x, y):\n",
    "    assert len(x.shape) == 2\n",
    "    assert len(y.shape) == 2\n",
    "    assert x.shape[1] == y.shape[0]\n",
    "    z = np.zeros((x.shape[0], y.shape[1])) \n",
    "    for i in range(x.shape[0]):\n",
    "        for j in range(y.shape[1]):\n",
    "            row_x = x[i, :]\n",
    "            column_y = y[:, j]\n",
    "            z[i, j] = naive_vector_dot(row_x, column_y)\n",
    "    return z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "guided-remains",
   "metadata": {},
   "source": [
    "为了理解点积形状的兼容性，它有助于通过对齐它们来可视化输入和输出张量，如图 2.5 所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "attempted-compilation",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gska6m4bphj30s00meab4.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eight-costa",
   "metadata": {},
   "source": [
    "x、 和 被描绘为矩形（系数的文字框）。 由于x 的行和 的 y列必须具有相同的大小，因此 的宽度必须与 的高度匹配。 y x y 如果您继续开发新的机器学习算法，您可能会经常绘制此类图表。\n",
    "\n",
    "更一般地，您可以在高维张量之间取点积，如下 与前面为 2D 情况概述的形状兼容性相同的规则："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "small-corruption",
   "metadata": {},
   "outputs": [],
   "source": [
    "(a, b, c, d) • (d,) -> (a, b, c)\n",
    "(a, b, c, d) • (d, e) -> (a, b, c, e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "african-seattle",
   "metadata": {},
   "source": [
    "### 张量整形"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "existing-husband",
   "metadata": {},
   "source": [
    "需要理解的第三种张量运算是张量整形。 尽管在我们的第一个神经网络示例中的层中没有使用它，但我们在将数字数据输入模型之前对数字数据进行 Dense 预处理时使用了它："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "subsequent-sheep",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_images = train_images.reshape((60000, 28 * 28))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "latest-lancaster",
   "metadata": {},
   "source": [
    "重塑张量意味着重新排列其行和列以匹配目标形状。 自然地，重新整形的张量与初始张量具有相同的系数总数。 通过简单的例子可以最好地理解重塑："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 158,
   "id": "backed-theme",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 365 µs (started: 2021-07-17 21:49:24 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.array([[0., 1.],\n",
    "               [2., 3.],\n",
    "               [4., 5.]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 159,
   "id": "boolean-assets",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(3, 2)"
      ]
     },
     "execution_count": 159,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.39 ms (started: 2021-07-17 21:49:28 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 160,
   "id": "preliminary-technology",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0.],\n",
       "       [1.],\n",
       "       [2.],\n",
       "       [3.],\n",
       "       [4.],\n",
       "       [5.]])"
      ]
     },
     "execution_count": 160,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.04 ms (started: 2021-07-17 21:49:38 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = x.reshape((6, 1))\n",
    "x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 161,
   "id": "modular-boards",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0., 1., 2.],\n",
       "       [3., 4., 5.]])"
      ]
     },
     "execution_count": 161,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.69 ms (started: 2021-07-17 21:49:55 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = x.reshape((2, 3))\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "homeless-costume",
   "metadata": {},
   "source": [
    "经常遇到的一种特殊的重塑情况是转置，转置矩阵意味着交换它的行和列，这样就变成了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 164,
   "id": "stock-imagination",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(300, 20)"
      ]
     },
     "execution_count": 164,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.41 ms (started: 2021-07-17 21:51:22 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.zeros((300, 20))\n",
    "x.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 165,
   "id": "polished-relief",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(20, 300)"
      ]
     },
     "execution_count": 165,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.11 ms (started: 2021-07-17 21:51:38 +08:00)\n"
     ]
    }
   ],
   "source": [
    "x = np.transpose(x)\n",
    "x.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "nasty-harassment",
   "metadata": {},
   "source": [
    "### 张量运算的几何解释"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "engaged-birth",
   "metadata": {},
   "source": [
    "因为张量运算所操纵的张量的内容可以解释为一些几何空间中点的坐标，所以所有的张量运算都有几何解释。 例如，让我们考虑加法。 我们将从以下向量开始："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 166,
   "id": "strategic-permission",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 306 µs (started: 2021-07-17 21:52:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "A = [0.5, 1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "identified-postcard",
   "metadata": {},
   "source": [
    "它是二维空间中的一个点（见图 2.6）。 通常将向量描绘为连接原点和点的箭头，如图 2.7 所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "trained-mustang",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskaewzyusj30h80m4mxl.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "helpful-dispute",
   "metadata": {},
   "source": [
    "让我们考虑一个新点，B = [1, 0.25]，我们将把它添加到前一个点上。 这是通过将矢量箭头链接在一起以几何方式完成的，结果位置是表示前两个矢量之和的矢量（见图 2.8）。 可以看到，在向量A上加上向量B代表了在新位置复制点A的动作，它到原点A的距离和方向由向量B决定。如果你将相同的向量加到一个组 平面中的点（一个“对象”），您将在新位置创建整个对象的副本（见图 2.9）。 因此，张量加法表示将对象（移动对象而不使其变形）在某个方向上平移一定量的动作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "assured-sterling",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskahbh25zj30ws0u0gmz.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "convenient-editing",
   "metadata": {},
   "source": [
    "一般而言，平移、旋转、缩放、倾斜等基本几何运算都可以表示为张量运算。 这里有一些例子。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "historical-digest",
   "metadata": {},
   "source": [
    "* 平移：正如你刚刚看到的，向一个点添加一个向量会在一个固定的方向上移动这个点一个固定的量。 应用于一组点（例如 2D 对象），这称为“平移”（见图 2.9）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "divided-temple",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskaj7k4foj31560mgmyn.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stainless-russell",
   "metadata": {},
   "source": [
    "* 旋转：可以通过一个 2 × 2 矩阵 R = [[cos(theta), sin(theta)], [-sin(theta) 的点积来实现 2D 向量旋转角度 theta（见图 2.10） ), cos(theta)]]。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "together-invalid",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskakniqqlj314c0l0jsy.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amber-anthony",
   "metadata": {},
   "source": [
    "* 缩放：图像的垂直和水平缩放（见图 2.11）可以通过具有 2 x 2 矩阵的点积来实现（请注意，这种矩阵称为“对角 S = 矩阵”，因为它只有非 其“对角线”中的系数为零，从左上角到右下角）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "architectural-mississippi",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskalhqmg6j314q0kujsl.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "equal-booking",
   "metadata": {},
   "source": [
    "* 线性变换：具有任意矩阵的点积实现了线性变换。请注意，上面看到的 和 是定义线性变换。 缩放旋转\n",
    "* 仿射变换：仿射变换（见图 2.12）是线性变换（通过一些矩阵的点积实现）和平移（通过向量加法实现）的组合。 您可能已经认识到，这正是该层实现的 y = W • x + b 计算！ 没有激活的层 Dense Dense\n",
    "函数是一个仿射层。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fluid-hampton",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskaml9oqij313w0i40ty.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "moral-mystery",
   "metadata": {},
   "source": [
    "如果你重复应用它们中的许多，你仍然会得到一个仿射变换（所以你可以首先应用那个仿射变换）。 让我们用两个来试试：affine2(affine1(x)) = W2 • (W1 • x + b1) + b2 = (W2 • Wa) • x + (W2 • b1 + b2)。 这是一个仿射变换，其中线性部分是矩阵 W2 • W1，平移部分是向量 W2 • b1 + b2。 因此，完全由没有激活的层组成的多层神经网络将等效于密集单层。 这个“深度”神经网络只是一个变相的线性模型！ 这就是我们需要激活函数的原因，例如（参见图 relu 2.13 中的操作）。 多亏了激活函数，可以制作一个层链来实现密集的非常复杂的非线性几何变换，从而为您的深度神经网络提供非常丰富的假设空间。 我们将在下一章更详细地介绍这个想法。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "intellectual-assessment",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskaook914j31580jadh7.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "continuing-effect",
   "metadata": {},
   "source": [
    "### 深度学习的几何解释"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "general-sponsorship",
   "metadata": {},
   "source": [
    "您刚刚了解到神经网络完全由张量运算链组成，所有这些张量运算只是输入数据的简单几何变换。因此，您可以将神经网络解释为高维空间中非常复杂的几何变换，通过一系列简单的步骤实现。\n",
    "\n",
    "在 3D 中，以下心理图像可能很有用。想象两张彩色纸：一张红色一张蓝色。把一个放在另一个上面。现在把它们揉成一个小球。那个皱巴巴的纸球是你的输入数据，每张纸都是分类问题中的一类数据。神经网络的目的是找出纸球的变形，将它弄皱，从而使这两个类再次完全分离。通过深度学习，这将作为 3D 空间的一系列简单变换来实现，例如您可以用手指在纸球上应用的变换，一次一个动作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "brutal-andrews",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gskarwqwnhj31500imtaf.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stunning-louisville",
   "metadata": {},
   "source": [
    "解开纸球是机器学习的意义所在：在高维空间（流形是一个连续的流形表面，就像我们皱巴巴的纸一样）为复杂的、高度折叠的数据找到整洁的表示。 在这一点上，您应该对深度学习为何擅长于此有一个很好的直觉：它采用的方法是将复杂的几何变换逐步分解为一长串基本变换，这几乎是人类遵循的策略 解开一个纸球。 深层网络中的每一层都应用了一种转换来稍微解开数据——而深层次的堆栈使一个极其复杂的解开过程变得容易处理。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "liberal-favor",
   "metadata": {},
   "source": [
    "## 神经网络引擎：基于梯度的优化"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "frozen-witness",
   "metadata": {},
   "source": [
    "正如您在上一节中看到的，我们第一个模型示例中的每个神经层都按如下方式转换其输入数据："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "about-recipient",
   "metadata": {},
   "outputs": [],
   "source": [
    "output = relu(dot(input, W) + b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "critical-reducing",
   "metadata": {},
   "source": [
    "在这个表达式中，W和b是作为层属性的张量。 它们被称为层的权重或可训练参数（分别为 和 属性）。 这些内核偏置权重包含模型从暴露于训练数据中学到的信息。\n",
    "\n",
    "最初，这些权重矩阵填充有小的随机值（称为随机初始化的步骤）。 当然，没有理由期望 relu(dot(input, W) + b) W ，当 和 是随机的，会产生任何有用的表示。 由此产生的表示是无意义的——但它们是一个起点。 接下来是根据反馈信号逐渐调整这些权重。 这种渐进式调整，也称为 ，基本上是机器学习的全部内容的训练学习。\n",
    "\n",
    "这发生在所谓的训练循环中，其工作原理如下。 循环重复这些步骤，直到损失看起来足够低：\n",
    "1. 绘制一批训练样本x和对应的目标y_true。\n",
    "2. 运行模型（称为 x 前向传递的步骤）以获得预测 y_pred。\n",
    "3. 计算批次上模型的损失，这是 y_pred 和 y_true 之间不匹配的度量。\n",
    "4. 以稍微减少该批次损失的方式更新模型的所有权重。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "floral-ebony",
   "metadata": {},
   "source": [
    "你最终会得到一个训练数据损失非常低的模型：预测 y_pred 和预期目标 y_true 之间的低不匹配。 该模型已经“学会”将其输入映射到正确的目标。 从远处看，它可能看起来像魔术，但是当您将其简化为基本步骤时，它就会变得很简单。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "public-stretch",
   "metadata": {},
   "source": [
    "第 1 步听起来很简单——只是 I/O 代码。 第 2 步和第 3 步只是少数张量运算的应用，因此您可以完全根据您在上一节中学到的知识来实现这些步骤。 困难的部分是第 4 步：更新模型的权重。 给定模型中的单个权重系数，您如何计算该系数是否应该增加或减少，以及增加多少？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fatty-rebound",
   "metadata": {},
   "source": [
    "一种简单的解决方案是冻结模型中除考虑的一个标量系数之外的所有权重，并为该系数尝试不同的值。 假设系数的初始值为 0.3。 前向传递一批数据后，该批上的模型损失为0.5。 如果您将系数的值更改为 0.35 并重新运行前向传递，损失将增加到 0.6。 但是如果你将系数降低到 0.25，损失就会下降到 0.4。 在这种情况下，似乎将系数更新 -0.05 将有助于最小化损失。 这必须对模型中的所有系数重复。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "transsexual-chase",
   "metadata": {},
   "source": [
    "但是这样的方法效率极低，因为您需要为每个单独的系数（其中有很多，通常是数千个，有时高达数百万个）计算两次前向传递（这是昂贵的）。 值得庆幸的是，有一个更好的方法：梯度\n",
    "下降。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "blessed-brooks",
   "metadata": {},
   "source": [
    "梯度下降是为现代神经网络提供动力的优化技术。这是它的要点。我们模型中使用的所有函数（例如 或 ）都以点 + 平滑和连续的方式转换其输入：例如，如果您查看 ，仅 z = x + y 的微小变化会导致z 的微小变化，如果你知道y中变化的方向，你就可以推断出z中变化的方向。在数学上，你会说这些函数是 z 可微的。如果将这些函数链接在一起，您获得的更大的函数仍然是可微的。特别是，这适用于将模型系数映射到模型在一批数据上的损失的函数：模型系数的微小变化会导致损失值发生微小的、可预测的变化。这使您能够使用称为 的数学运算符来描述当您在不同方向上移动模型系数时损失的梯度如何变化。如果您计算此梯度，您可以使用它在减少损失的方向上移动系数（在一次更新中一次全部完成，而不是一次一个）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "starting-league",
   "metadata": {},
   "source": [
    "如果您已经知道可微分的含义和 a 是什么，您可以跳到第 2.4 节梯度。 否则，以下两节将帮助您理解这些概念。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "inner-million",
   "metadata": {},
   "source": [
    "### 什么是导数？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "vietnamese-north",
   "metadata": {},
   "source": [
    "考虑一个连续、平滑的函数，将一个数字x映射到一个新数字y。 我们 f(x) = y 可以使用下面的函数作为例子："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exciting-preservation",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm5zeuenoj310w0mmmxt.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "alive-owner",
   "metadata": {},
   "source": [
    "因为函数是连续的，x 的微小变化只能导致 y 连续的微小变化——这就是连续性背后的直觉。 假设您对x增加了一个很小的epsilon_x ，这导致y有一个小的epsilon_y变化："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "judicial-placement",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm622j9x3j310y0lwgmn.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "neutral-spider",
   "metadata": {},
   "source": [
    "另外，因为函数是光滑的（它的曲线没有任何陡峭的角度），当epsilon_x足够小时，在某个点附近，f可以近似为斜率的线性p函数，这样epsilon_y 就变成了一个a * epsilon_x:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "running-meeting",
   "metadata": {},
   "outputs": [],
   "source": [
    "f(x + epsilon_x) = y + a * epsilon_x"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cognitive-original",
   "metadata": {},
   "source": [
    "显然，这种线性近似只有x在足够接近p时才有效。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "automated-girlfriend",
   "metadata": {},
   "source": [
    "斜率 a 称为 p 中 f 的斜率。 如果为负，则表示导数 f p a x p 附近的微小变化将导致减少（如图 2.17 所示）； 如果为正，则 f(x) a in 的微小变化将导致 的增加。 此外，（x f(x) 的幅度导数）的绝对值告诉您这种增加或减少发生的速度。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "incomplete-pregnancy",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm69buf6jj311e0o0wff.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "trying-serve",
   "metadata": {},
   "source": [
    "> 省略部分关于微分的内容，因为大学都学过"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "academic-mission",
   "metadata": {},
   "source": [
    "### 张量运算的导数：梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "neither-drove",
   "metadata": {},
   "source": [
    "> 省略"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "innocent-state",
   "metadata": {},
   "source": [
    "### 随机梯度下降"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "loose-purchase",
   "metadata": {},
   "source": [
    "给定一个可微函数，理论上可以通过解析找到它的最小值：众所周知，函数的最小值是导数为 0 的点，因此您要做的就是找到导数为 0 的所有点并检查哪个点 在这些点中，该函数的值最低。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "developmental-moldova",
   "metadata": {},
   "source": [
    "应用于神经网络，这意味着通过分析找到产生最小可能损失函数的权重值组合。 这可以通过求解方程 grad(f(W), W) = 0 来完成。 这是变量N的多项式方程，其中N是模型中系数的数量 。 尽管可以解出 N = 2 或 的这样的方程，但对于真实的神经网络来说，这样做是棘手的，其中参数的数量是 N = 3，从不低于几千个，通常可以达到几千万。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "american-effectiveness",
   "metadata": {},
   "source": [
    "相反，您可以使用本节开头概述的四步算法：根据随机批次数据的当前损失值一点一点地修改参数。 因为你正在处理一个可微函数，你可以计算它的梯度，这为你提供了一种实现步骤 4 的有效方法。 如果你在梯度的相反方向更新权重，每次损失都会少一点：\n",
    "1. 绘制一批训练样本x和对应的目标y_true。\n",
    "2. 在 x 上运行模型以获得预测（这称为 y_pred 前向传递）。\n",
    "3. 计算批次上模型的损失，这是 y_pred 和 y_true 之间不匹配的度量。\n",
    "4. 计算关于模型参数的损失梯度（这称为反向传播）。\n",
    "5. 在与梯度相反的方向稍微移动参数，例如 W -= learning_rate * gradient - 从而稍微减少批次的损失。 学习率（这里是learning_rate）将是一个调节“速度”的标量因子梯度下降过程。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "reduced-privacy",
   "metadata": {},
   "source": [
    "够简单！ 我们刚刚描述的称为小批量随机梯度下降（小批量 SGD）。 该术语指的是每批数据都是随机抽取的（是 的科学同义词）。 图 2.18 说明了在随机随机一维中会发生什么，当模型只有一个参数而您只有一个训练样本时。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efficient-virginia",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm6j1y25zj30y30u0ta7.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "burning-cancellation",
   "metadata": {},
   "source": [
    "如您所见，直观地为 learning_rate 因子选择一个合理的值很重要。 如果它太小，沿着曲线下降将需要多次迭代，并且可能会陷入局部最小值。 如果 learning_rate 太大，您的更新最终可能会将您带到曲线上完全随机的位置。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "collectible-manual",
   "metadata": {},
   "source": [
    "请注意，小批量 SGD 算法的一个变体是在每次迭代时绘制单个样本和目标，而不是绘制一批数据。 这将是 SGD（与真正的小批量 SGD 相反）。 或者，走向相反的极端，您可以对所有可用数据运行每一步，这称为批量梯度下降。 每次更新都会更准确，但要贵得多。 这两个极端之间的有效折衷是使用合理大小的小批量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "middle-survey",
   "metadata": {},
   "source": [
    "虽然图 2.18 展示了在一维参数空间中的梯度下降，但实际上你将在高维空间中使用梯度下降：神经网络中的每个权重系数都是空间中的一个自由维度，可能有数万甚至数百万 其中。 为了帮助您建立对损失表面的直觉，您还可以可视化沿 2D 损失表面的梯度下降，如图 2.19 所示。 但是你不可能想象出训练神经网络的实际过程是什么样的——你不能以一种对人类有意义的方式来表示一个 1,000,000 维的空间。 因此，请记住，您通过这些低维表示形成的直觉在实践中可能并不总是准确的。 从历史上看，这一直是深度学习研究领域问题的根源。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "instant-girlfriend",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm6qkwr29j313s0rwwgl.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dutch-schema",
   "metadata": {},
   "source": [
    "此外，存在多种 SGD 变体，它们在计算下一次权重更新时会考虑先前的权重更新，而不仅仅是查看梯度的当前值。 例如，有动量的 SGD，以及 Adagrad、RMSprop 和其他几个。 这种变体被称为优化方法优化器或。 特别是，在许多这些变体中使用的 的概念值得您注意。 Momentum 解决了 SGD 的两个问题：收敛速度和局部最小值。 考虑图 2.20，它显示了作为模型参数函数的损失曲线。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "informative-power",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm6rqfeofj31480osgmx.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "martial-juice",
   "metadata": {},
   "source": [
    "如您所见，在某个参数值附近，有一个局部最小值：在该点附近，向左移动会导致损失增加，但向右移动也会导致损失增加。 如果正在考虑的参数通过 SGD 以较小的学习率进行优化，那么优化过程将卡在局部最小值而不是达到全局最小值。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "imported-copyright",
   "metadata": {},
   "source": [
    "您可以通过使用从物理学中汲取灵感的动量来避免此类问题。 这里一个有用的心理形象是将优化过程视为一个沿着损失曲线滚动的小球。 如果它有足够的动量，球就不会卡在峡谷中，最终会达到全局最小值。 动量是通过在每一步移动球来实现的，不仅基于当前的斜率值（当前加速度），还基于当前速度（由过去的加速度产生）。 实际上，这意味着不仅要根据当前的 w 梯度值更新参数，还要根据先前的参数更新来更新参数，例如在这个简单的实现中："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ethical-biotechnology",
   "metadata": {},
   "outputs": [],
   "source": [
    "past_velocity = 0.\n",
    "momentum = 0.1\n",
    "while loss > 0.01:\n",
    "    w, loss, gradient = get_current_parameters()\n",
    "    velocity = past_velocity * momentum - learning_rate * gradient\n",
    "    w = w + momentum * velocity - learning_rate * gradient\n",
    "    past_velocity = velocity\n",
    "    update_parameter(w)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bridal-cannon",
   "metadata": {},
   "source": [
    "### 链式导数：反向传播算法"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rapid-devon",
   "metadata": {},
   "source": [
    "在上面的算法中，我们随便假设，因为一个函数是可微的，我们可以很容易地计算它的梯度。 但这是真的吗？ 在实践中我们如何计算复杂表达式的梯度？ 在我们的两层网络示例中，我们如何获得与权重有关的损失梯度？ 这就是反向传播算法的用武之地。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "burning-correspondence",
   "metadata": {},
   "source": [
    "**链式法则**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "superb-dodge",
   "metadata": {},
   "source": [
    "反向传播是一种使用简单操作（例如加法、relu 或张量积）的导数来轻松计算这些原子操作的任意复杂组合的梯度的方法。 至关重要的是，神经网络由许多链接在一起的张量运算组成，每个张量运算都有一个简单的、已知的导数。 例如，我们第一个示例中的模型可以表示为由变量W1 b1 W2和b2（分别属于第一层和第二层Dense顶部）参数化的函数，涉及原子操作dot，relu，softmax和+，以及我们的损失函数loss，它们都很容易区分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "remarkable-adobe",
   "metadata": {},
   "outputs": [],
   "source": [
    "loss_value = loss(y_true, softmax(dot(relu(dot(inputs, W1) + b1), W2) + b2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "limited-nation",
   "metadata": {},
   "source": [
    "微积分告诉我们，可以使用以下恒等式推导出这样的函数链，称为 ："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "discrete-initial",
   "metadata": {},
   "source": [
    "考虑两个函数f和g，以及组合函数fg，使得 y = fg(x) == f(g(x))："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "behind-conservation",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fg(x):\n",
    "    x1 = g(x)\n",
    "    y = f(x1)\n",
    "    return y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "piano-brown",
   "metadata": {},
   "source": [
    "然后链式法则指出 grad(y, x) == grad(y, x1) * grad(x1, x)。 只要您知道 f 和 g 的导数，就可以计算出 fg 的导数。 链式规则的命名是这样的，因为当你添加更多的中间函数时，它开始看起来像一个链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "mighty-external",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fghj(x):\n",
    "    x1 = j(x)\n",
    "    x2 = h(x1)\n",
    "    x3 = g(x2)\n",
    "    y = f(x3)\n",
    "    return y\n",
    "grad(y, x) == grad(y, x3) * grad(x3, x2) * grad(x2, x1) * grad(x1, x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lonely-binding",
   "metadata": {},
   "source": [
    "将链式法则应用于神经网络梯度值的计算，产生了一种称为反向传播的算法。 让我们具体看看它是如何工作的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "executed-nickel",
   "metadata": {},
   "source": [
    "**使用计算图进行自动微分**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bearing-sustainability",
   "metadata": {},
   "source": [
    "考虑反向传播的一种有用方法是计算图。 计算图是 TensorFlow 和深度学习革命的核心数据结构。 它是一个有向无环运算图——在我们的例子中是张量运算。 例如，这是我们第一个模型的图形表示："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "precise-wireless",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm7d5g6hyj30iu0m23z7.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "legislative-intelligence",
   "metadata": {},
   "source": [
    "计算图在计算机科学中是一个非常成功的抽象，因为它们使我们能够将计算视为数据：可计算表达式被编码为机器可读的数据结构，可以用作另一个程序的输入或输出。 例如，您可以想象一个程序接收一个计算图并返回一个新的计算图，该计算图实现了相同计算的大规模分布式版本——这意味着您可以分发任何计算而无需自己编写分发逻辑。 或者想象一下……一个接收计算图并可以自动生成它所代表的表达式的导数的程序。 如果您的计算表示为显式图形数据结构，而不是 .py 文件中的 ASCII 字符行，那么做这些事情会容易得多。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "strategic-department",
   "metadata": {},
   "source": [
    "为了清楚地解释反向传播，让我们看一个计算图的非常基本的例子。 我们将考虑上图的简化版本，其中我们只有一个线性层并且所有变量都是标量。 我们将采用两个标量变量 w、b，一个标量输入 x，并对它们应用一些操作以组合成输出 y。 最后，我们将应用绝对值误差损失函数：loss_val = abs(y_true - y)。 由于我们希望以最小化 loss_val 的方式更新 w 和 b，因此我们对计算 grad(loss_val, b) 和 grad(loss_val, w) 感兴趣。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "placed-violin",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm7f17o5jj30oe0qmdgm.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "light-proposition",
   "metadata": {},
   "source": [
    "让我们为图 x 中的“输入节点”设置具体值，即输入 x、目标 y_true、w 和 b。 我们将这些值从上到下传播到图中的所有节点，直到达到 。 这是 loss_val 前向传递。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "welcome-barbados",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm7h150ehj30ks0oymxx.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "intense-highlight",
   "metadata": {},
   "source": [
    "现在让我们“反转”图：对于图中从 a 到 b 的每条边，我们将创建一条从 b 到 a 的相反边，并询问“当 a 变化时 b 变化多少？” 也就是说，什么是 grad(b, a)？ 我们将用这个值注释每条倒边。 这个反向图代表反向传播。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "blond-warrior",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm7ipqernj30z80p4q4a.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "gross-money",
   "metadata": {},
   "source": [
    "我们有：\n",
    "* grad(loss_val, x2) = 1，因为x2 随着 epsilon 的变化，loss_val = abs(4 - x2) 也有相同的变化。\n",
    "* grad(x2, x1) = 1，因为x1随着 epsilon 量的变化，x2 = x1 + b = x1 +1 变化量相同。\n",
    "* grad(x2, b) = 1，因为b随着 epsilon 量的变化，x2 = x1 + b = 6 + b 变化量相同。\n",
    "* grad(x1, w) = 2，因为w随着数量 epsilon 的变化，x1 = x * w = 2 * w 变化了 2 * epsilon。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "weekly-needle",
   "metadata": {},
   "source": [
    "链式法则对这个反向图说的是，您可以通过将连接两个节点的路径上的每条边的导数相乘来获得一个节点相对于另一个节点的导数。 例如，grad(loss_val, w) = grad(loss_val, x2) * grad(x2, x1) * grad(x1, w)。 （见图 2.25）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "worst-interface",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm7l9s9t0j311q0ni75k.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "least-suspect",
   "metadata": {},
   "source": [
    "通过将链式法则应用到我们的图中，我们得到了我们想要的东西：\n",
    "* grad(loss_val, w) = 1 * 1 * 2 = 2\n",
    "* grad(loss_val, b) = 1 * 1 = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fluid-potato",
   "metadata": {},
   "source": [
    "> 如果在反向图中有多个路径连接两个感兴趣的节点 a, b，我们将通过对所有路径的贡献求和来获得 grad(b, a)。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "existing-coupon",
   "metadata": {},
   "source": [
    "有了这个，你就看到了反向传播的作用！ 反向传播只是将链式法则应用于计算图。 仅此而已。 反向传播从最终的损失值开始，从顶层到底层反向传播，计算每个参数对损失值的贡献。 这就是“反向传播”名称的由来：我们“反向传播”计算图中不同节点的损失贡献。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "painful-festival",
   "metadata": {},
   "source": [
    "现在和未来几年，人们在能够自动微分的现代框架中实现神经网络，例如 TensorFlow。 自动微分是通过上面介绍的那种计算图来实现的。 自动微分使得检索可微张量操作的任意组合的梯度成为可能，除了写下前向传递外，无需做任何额外的工作。 当我在 2000 年代用 C 编写我的第一个神经网络时，我不得不手动编写我的梯度。 现在，多亏了现代自动微分工具，您将永远不必自己实现反向传播。 认为自己很幸运！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "brilliant-pennsylvania",
   "metadata": {},
   "source": [
    "**Tensorflow中的梯度tape**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "premium-throw",
   "metadata": {},
   "source": [
    "您可以利用 TensorFlow 强大的自动微分功能的 API 是 GradientTape。 它是一个 Python 作用域，它将以计算图（有时称为“磁带”）的形式“记录”在其中运行的张量操作。 然后，该图可用于检索任何输出相对于任何变量或变量集(tf.Variable 类的实例）的梯度。 tf.Variable是一种特定类型的张量，用于保持可变状态——例如，神经网络的权重总是 tf.Variable 实例。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "plastic-highway",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "x = tf.Variable(0.) \n",
    "with tf.GradientTape() as tape: \n",
    "    y = 2 * x + 3 \n",
    "grad_of_y_wrt_x = tape.gradient(y, x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "palestinian-seating",
   "metadata": {},
   "source": [
    "1. 实例化一个标量，初始值为 0 变量\n",
    "2. 打开 GradientTape 范围\n",
    "3. 在作用域内，对我们的变量应用一些张量操作\n",
    "4. 使用磁带检索输出相对于我们的变量 y x 的梯度"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "backed-greenhouse",
   "metadata": {},
   "source": [
    "GradientTape 使用张量操作："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "divine-surrey",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = tf.Variable(tf.random.uniform((2, 2))) \n",
    "with tf.GradientTape() as tape:\n",
    "    y = 2 * x + 3\n",
    "grad_of_y_wrt_x = tape.gradient(y, x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sought-animal",
   "metadata": {},
   "source": [
    "1. 实例化一个具有形状和初始值全零的变量 (2, 2)\n",
    "2. grad_of_y_wrt_x 是一个形状 (2, 2)（如 x）的张量，描述了 y = 2 * a + 3 x = [[0, 0], [0, 0]] 周围的曲率"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "organic-clark",
   "metadata": {},
   "source": [
    "它还适用于变量列表："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "hidden-kingston",
   "metadata": {},
   "outputs": [],
   "source": [
    "W = tf.Variable(tf.random.uniform((2, 2)))\n",
    "b = tf.Variable(tf.zeros((2,)))\n",
    "x = tf.random.uniform((2, 2))\n",
    "with tf.GradientTape() as tape:\n",
    "    y = tf.matmul(x, W) + b\n",
    "grad_of_y_wrt_W_and_b = tape.gradient(y, [W, b])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tracked-correction",
   "metadata": {},
   "source": [
    "* grad_of_y_wrt_W_and_b 是两个张量的列表，分别与 W和b 具有相同的形状"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "accessory-alarm",
   "metadata": {},
   "source": [
    "您将在下一章中了解梯度胶带。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pending-arbitration",
   "metadata": {},
   "source": [
    "## 回顾我们的第一个例子"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "incomplete-pipeline",
   "metadata": {},
   "source": [
    "您已接近本章的结尾，现在您应该对神经网络的幕后发生的事情有一个大致的了解。 本章开头的神奇黑匣子已经变成了更清晰的图景，如图 2.26 所示：模型由链接在一起的层组成，将输入数据映射到预测。 然后，损失函数将这些预测与目标进行比较，产生一个损失值：衡量模型预测与预期匹配程度的度量。 优化器使用这个损失值来更新模型的权重。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "medium-projector",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gsm82d0rchj30ss0m40u9.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exact-flood",
   "metadata": {},
   "source": [
    "让我们回到第一个示例并根据您在前几节中学到的内容来回顾它的每一部分。\n",
    "这是输入数据："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "wired-detective",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 380 ms (started: 2021-07-19 14:12:06 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.datasets import mnist\n",
    "\n",
    "(train_images, train_labels), (test_images, test_labels) = mnist.load_data()\n",
    "train_images = train_images.reshape((60000, 28 * 28))\n",
    "train_images = train_images.astype(\"float32\") / 255\n",
    "test_images = test_images.reshape((10000, 28 * 28))\n",
    "test_images = test_images.astype(\"float32\") / 255"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stretch-regular",
   "metadata": {},
   "source": [
    "现在您了解输入图像存储在 NumPy 张量中，这里的张量格式分别为 float32 (60000, 784)（训练数据）和（10000, 784）（测试数据）形状的张量。\n",
    "\n",
    "这是我们的模型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "continued-galaxy",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 251 µs (started: 2021-07-19 14:12:08 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras import models, layers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "middle-novelty",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 16.2 ms (started: 2021-07-19 14:13:59 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model = models.Sequential([\n",
    "    layers.Dense(512, activation=\"relu\", input_shape=(784,)),\n",
    "    layers.Dense(10, activation=\"softmax\")\n",
    "])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "patent-handle",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"sequential_3\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "dense_7 (Dense)              (None, 512)               401920    \n",
      "_________________________________________________________________\n",
      "dense_8 (Dense)              (None, 10)                5130      \n",
      "=================================================================\n",
      "Total params: 407,050\n",
      "Trainable params: 407,050\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n",
      "time: 2.46 ms (started: 2021-07-19 14:14:01 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "retired-sailing",
   "metadata": {},
   "source": [
    "现在你明白这个模型由两层链组成，每一层 Dense 对输入数据应用一些简单的张量操作，这些操作涉及权重张量。 作为层的属性的权重张量是模型知识持续存在的地方。\n",
    "\n",
    "这是模型编译步骤："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "arranged-knife",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.64 ms (started: 2021-07-19 14:14:04 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.compile(optimizer=\"rmsprop\",\n",
    "              loss=\"sparse_categorical_crossentropy\",\n",
    "              metrics=[\"accuracy\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "historic-president",
   "metadata": {},
   "source": [
    "现在您了解 sparse_categorical_crossentropy 是用作学习权重张量的反馈信号的损失函数，训练阶段将尝试将其最小化。 您还知道，这种损失的减少是通过小批量随机梯度下降实现的。 控制梯度下降的特定使用的确切规则由作为第一个参数传递的 rmsprop 优化器定义。 \n",
    "\n",
    "最后，这是训练循环："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "gorgeous-oklahoma",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5\n",
      "469/469 [==============================] - 4s 7ms/step - loss: 0.2579 - accuracy: 0.9248\n",
      "Epoch 2/5\n",
      "469/469 [==============================] - 4s 8ms/step - loss: 0.1046 - accuracy: 0.9690\n",
      "Epoch 3/5\n",
      "469/469 [==============================] - 4s 7ms/step - loss: 0.0682 - accuracy: 0.9793\n",
      "Epoch 4/5\n",
      "469/469 [==============================] - 4s 7ms/step - loss: 0.0494 - accuracy: 0.9850\n",
      "Epoch 5/5\n",
      "469/469 [==============================] - 4s 7ms/step - loss: 0.0371 - accuracy: 0.9888\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<tensorflow.python.keras.callbacks.History at 0x7f06ac037490>"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 18.1 s (started: 2021-07-19 14:14:06 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.fit(train_images, train_labels, epochs=5, batch_size=128)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "blessed-crime",
   "metadata": {},
   "source": [
    "现在您了解调用 时会发生什么：模型将开始以 128 个样本的小批量对拟合训练数据进行迭代，重复 5 次（对所有训练数据的每次迭代称为 ）。 对于每个批次，该模型将计算与权重有关的历元的损失梯度（使用反向传播算法，该算法源自微积分中的链式法则），并在将减少损失值的方向上移动权重 对于这批。\n",
    "\n",
    "在这 5 个 epoch 之后，该模型将执行 2,345 次梯度更新（每个 epoch 469 次），并且模型的损失将足够低，该模型将能够以高精度对手写数字进行分类。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "perfect-identification",
   "metadata": {},
   "source": [
    "在这一点上，您已经了解了有关神经网络的大部分知识。 让我们通过在 TensorFlow 中“从头开始”逐步重新实现第一个示例的简化版本来证明这一点。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "olive-legend",
   "metadata": {},
   "source": [
    "### 在 TensorFlow 中从头开始重新实现我们的第一个示例"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unexpected-mechanism",
   "metadata": {},
   "source": [
    "有什么比从头开始实施所有内容更好地展示完整、明确的理解？ 当然，这里的“从头开始”是相对的：我们不会重新实现基本的张量操作，也不会实现反向传播。 但是我们将进入如此低的级别，以至于我们几乎不会使用任何 Keras 功能。\n",
    "\n",
    "如果您还不了解此示例中的每个小细节，请不要担心。 下一章将更详细地介绍 TensorFlow API。 现在，请尝试遵循正在发生的事情的要点——这个例子的目的是帮助你使用具体的实现来明确你对深度学习数学的理解。 我们走吧！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "different-monte",
   "metadata": {},
   "source": [
    "**一个简单的全连接类**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "green-shoot",
   "metadata": {},
   "source": [
    "您之前已经了解到，该层实现了以下输入转换，Dense，其中W和b是模型参数，并且激活是一个元素函数（通常是\n",
    "relu，但它会用于最后一层）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "traditional-relevance",
   "metadata": {},
   "outputs": [],
   "source": [
    "output = activation(dot(W, input) + b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "another-commissioner",
   "metadata": {},
   "source": [
    "让我们实现一个简单的 Python 类 NaiveDense，它创建两个 TensorFlow 变量和 W b，并公开一个应用上述转换的方法call。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "above-history",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 735 µs (started: 2021-07-19 17:31:05 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "class NaiveDense:\n",
    "    def __init__(self, input_size, output_size, activation):\n",
    "        self.activation = activation\n",
    "        w_shape = (input_size, output_size)\n",
    "        w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)\n",
    "        self.W = tf.Variable(w_initial_value)\n",
    "        b_shape = (output_size,)\n",
    "        b_initial_value = tf.zeros(b_shape)\n",
    "        self.b = tf.Variable(b_initial_value)\n",
    "        \n",
    "    def __call__(self, inputs):\n",
    "        return self.activation(tf.matmul(inputs, self.W) + self.b)\n",
    "    \n",
    "    @property\n",
    "    def weights(self):\n",
    "        return [self.W, self.b]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "convinced-blackjack",
   "metadata": {},
   "source": [
    "1. 创建一个形状为“(input_size, output_size)”的矩阵，用随机 W 值初始化。\n",
    "2. 创建一个形状为 b (output_size,) 的向量，初始化为零。\n",
    "3. 应用前向传播。\n",
    "4. 检索图层权重的便捷方法。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "entire-swift",
   "metadata": {},
   "source": [
    "**一个简单的序列类**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "necessary-accent",
   "metadata": {},
   "source": [
    "现在，让我们创建一个 NaiveSequential 类来链接这些层。 它包装了一个层列表，并公开了一个方法，这些方法只是按顺序调用输入上的底层层。 它还调用 features 一个属性来轻松跟踪图层的参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "removable-overview",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 635 µs (started: 2021-07-19 17:31:38 +08:00)\n"
     ]
    }
   ],
   "source": [
    "class NaiveSequential:\n",
    "    def __init__(self, layers):\n",
    "        self.layers = layers\n",
    "        \n",
    "    def __call__(self, inputs):\n",
    "        x = inputs\n",
    "        for layer in self.layers:\n",
    "            x = layer(x)\n",
    "        return x\n",
    "\n",
    "    @property\n",
    "    def weights(self):\n",
    "        weights = []\n",
    "        for layer in self.layers:\n",
    "            weights += layer.weights\n",
    "            return weights"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pursuant-fraction",
   "metadata": {},
   "source": [
    "使用这个 NaiveDense 类和这个 NaiveSequential 类，我们可以创建一个模拟 Keras 模型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "broad-failing",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.62 ms (started: 2021-07-19 17:38:14 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model = NaiveSequential([\n",
    "    NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),\n",
    "    NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)\n",
    "])\n",
    "assert len(model.weights) == 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "related-myanmar",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<tf.Variable 'Variable:0' shape=(784, 512) dtype=float32, numpy=\n",
       " array([[0.05474288, 0.03334844, 0.05399695, ..., 0.02889257, 0.09583582,\n",
       "         0.00209544],\n",
       "        [0.00046968, 0.04355458, 0.07128068, ..., 0.04350798, 0.00625761,\n",
       "         0.02799137],\n",
       "        [0.06705492, 0.09827209, 0.04390842, ..., 0.05562458, 0.08269938,\n",
       "         0.04494333],\n",
       "        ...,\n",
       "        [0.01507559, 0.06134799, 0.05574847, ..., 0.09836749, 0.04171026,\n",
       "         0.09820703],\n",
       "        [0.09151063, 0.02694974, 0.04601102, ..., 0.02695199, 0.04244385,\n",
       "         0.00907236],\n",
       "        [0.06890026, 0.03398303, 0.04653349, ..., 0.05811543, 0.08874077,\n",
       "         0.07099994]], dtype=float32)>,\n",
       " <tf.Variable 'Variable:0' shape=(512,) dtype=float32, numpy=\n",
       " array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
       "        0., 0.], dtype=float32)>]"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 11.3 ms (started: 2021-07-19 17:37:35 +08:00)\n"
     ]
    }
   ],
   "source": [
    "model.weights"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "durable-pacific",
   "metadata": {},
   "source": [
    "**批处理生成器**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "persistent-salad",
   "metadata": {},
   "source": [
    "接下来，我们需要一种以小批量迭代 MNIST 数据的方法。 这很简单"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "infectious-summary",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.02 ms (started: 2021-07-20 00:02:34 +08:00)\n"
     ]
    }
   ],
   "source": [
    "class BatchGenerator:\n",
    "    def __init__(self, images, labels, batch_size=128):\n",
    "        self.index = 0\n",
    "        self.images = images\n",
    "        self.labels = labels\n",
    "        self.batch_size = batch_size\n",
    "        \n",
    "    def next(self):\n",
    "        images = self.images[self.index : self.index + self.batch_size]\n",
    "        labels = self.labels[self.index : self.index + self.batch_size]\n",
    "        self.index += self.batch_size\n",
    "        return images, labels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "thick-training",
   "metadata": {},
   "source": [
    "### 运行一个训练步骤"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tough-cookbook",
   "metadata": {},
   "source": [
    "该过程中最困难的部分是“训练步骤”：在一批数据上运行模型后更新模型的权重。 我们需要：\n",
    "1. 计算模型对批次中图像的预测\n",
    "2. 给定实际标签，计算这些预测的损失值\n",
    "3. 计算关于模型权重的损失梯度\n",
    "4. 沿与梯度相反的方向少量移动权重\n",
    "\n",
    "为了计算梯度，我们将使用我们在 2.4.4 节中介绍的 TensorFlow GradientTape 对象。 像这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "sized-county",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 844 µs (started: 2021-07-20 00:02:21 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def one_training_step(model, images_batch, labels_batch):\n",
    "    # 运行“前向传递”（在 GradienTape 范围内计算模型的预测）\n",
    "    with tf.GradientTape() as tape:\n",
    "        predictions = model(images_batch)\n",
    "        per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(\n",
    "            labels_batch, predictions)\n",
    "        average_loss = tf.reduce_mean(per_sample_losses)\n",
    "        \n",
    "        #计算关于权重的损失梯度。 输出梯度是一个列表，其中每个条目对应于 model.weights 列表中的一个权重。\n",
    "        gradients = tape.gradient(average_loss, model.weights)\n",
    "        #使用梯度更新权重（我们将在下面定义这个函数）\n",
    "        update_weights(gradients, model.weights)\n",
    "    return average_loss"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "wanted-lawrence",
   "metadata": {},
   "source": [
    "正如您已经知道的那样，“权重更新”步骤（上面由 update_weights 函数表示）的目的是将权重向一个方向移动“一点”，以减少该批次的损失。 移动的幅度由“学习率”决定，通常是一个很小的数量。 实现这个 update_weights 函数的最简单方法是从每个权重中减去梯度 * learning_rate："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "driving-indication",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 762 µs (started: 2021-07-20 00:02:18 +08:00)\n"
     ]
    }
   ],
   "source": [
    "learning_rate = 1e-3\n",
    "def update_weights(gradients, weights):\n",
    "    for g, w in zip(gradients, model.weights):\n",
    "        #assign_sub 相当于 TensorFlow 变量。\n",
    "        w.assign_sub(g * learning_rate)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "incorporate-tractor",
   "metadata": {},
   "source": [
    "在实践中，您几乎永远不会手动实现这样的权重更新步骤。 相反，您将使用来自 Keras 的实例。 像这样：优化器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "geological-wallpaper",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.37 ms (started: 2021-07-20 00:02:14 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras import optimizers\n",
    "\n",
    "optimizer = optimizers.SGD(learning_rate=1e-3)\n",
    "def update_weights(gradients, weights):\n",
    "    optimizer.apply_gradients(zip(gradients, weights))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "subjective-stream",
   "metadata": {},
   "source": [
    "现在我们的每批次训练步骤已经准备就绪，我们可以将一个步骤转移到实施整个训练时期。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "civic-bunny",
   "metadata": {},
   "source": [
    "### 完整训练周期"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rubber-integration",
   "metadata": {},
   "source": [
    "一个训练周期只是对训练数据中的每一批次重复训练步骤，而完整的训练循环只是一个周期的重复："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "corrected-bolivia",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.19 ms (started: 2021-07-20 00:02:11 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def fit(model, images, labels, epochs, batch_size=128):\n",
    "    for epoch_counter in range(epochs):\n",
    "        print(f\"Epoch {epoch_counter}\")\n",
    "        batch_generator = BatchGenerator(images, labels)\n",
    "        for batch_counter in range(len(images) // batch_size):\n",
    "            images_batch, labels_batch = batch_generator.next()\n",
    "            loss = one_training_step(model, images_batch, labels_batch)\n",
    "            if batch_counter % 100 == 0:\n",
    "                print(f\"loss at batch {batch_counter}: {loss:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fixed-heavy",
   "metadata": {},
   "source": [
    "让我们试驾一下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "ambient-tobago",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0\n",
      "loss at batch 0: 4.43\n",
      "loss at batch 100: 3.75\n",
      "loss at batch 200: 3.74\n",
      "loss at batch 300: 3.07\n",
      "loss at batch 400: 3.27\n",
      "Epoch 1\n",
      "loss at batch 0: 2.79\n",
      "loss at batch 100: 2.72\n",
      "loss at batch 200: 2.81\n",
      "loss at batch 300: 2.44\n",
      "loss at batch 400: 2.60\n",
      "Epoch 2\n",
      "loss at batch 0: 2.26\n",
      "loss at batch 100: 2.24\n",
      "loss at batch 200: 2.27\n",
      "loss at batch 300: 2.04\n",
      "loss at batch 400: 2.14\n",
      "Epoch 3\n",
      "loss at batch 0: 1.91\n",
      "loss at batch 100: 1.92\n",
      "loss at batch 200: 1.90\n",
      "loss at batch 300: 1.78\n",
      "loss at batch 400: 1.84\n",
      "Epoch 4\n",
      "loss at batch 0: 1.68\n",
      "loss at batch 100: 1.71\n",
      "loss at batch 200: 1.67\n",
      "loss at batch 300: 1.61\n",
      "loss at batch 400: 1.65\n",
      "Epoch 5\n",
      "loss at batch 0: 1.53\n",
      "loss at batch 100: 1.57\n",
      "loss at batch 200: 1.51\n",
      "loss at batch 300: 1.49\n",
      "loss at batch 400: 1.52\n",
      "Epoch 6\n",
      "loss at batch 0: 1.42\n",
      "loss at batch 100: 1.46\n",
      "loss at batch 200: 1.39\n",
      "loss at batch 300: 1.40\n",
      "loss at batch 400: 1.43\n",
      "Epoch 7\n",
      "loss at batch 0: 1.34\n",
      "loss at batch 100: 1.37\n",
      "loss at batch 200: 1.30\n",
      "loss at batch 300: 1.32\n",
      "loss at batch 400: 1.35\n",
      "Epoch 8\n",
      "loss at batch 0: 1.26\n",
      "loss at batch 100: 1.30\n",
      "loss at batch 200: 1.22\n",
      "loss at batch 300: 1.25\n",
      "loss at batch 400: 1.28\n",
      "Epoch 9\n",
      "loss at batch 0: 1.20\n",
      "loss at batch 100: 1.24\n",
      "loss at batch 200: 1.15\n",
      "loss at batch 300: 1.19\n",
      "loss at batch 400: 1.23\n",
      "time: 23.5 s (started: 2021-07-20 00:02:41 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.datasets import mnist\n",
    "(train_images, train_labels), (test_images, test_labels) = mnist.load_data()\n",
    "train_images = train_images.reshape((60000, 28 * 28))\n",
    "train_images = train_images.astype(\"float32\") / 255\n",
    "test_images = test_images.reshape((10000, 28 * 28))\n",
    "test_images = test_images.astype(\"float32\") / 255\n",
    "\n",
    "fit(model, train_images, train_labels, epochs=10, batch_size=128)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "described-dealing",
   "metadata": {},
   "source": [
    "### 评估模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fluid-genesis",
   "metadata": {},
   "source": [
    "我们可以通过对测试图像进行预测的 argmax 并将其与预期标签进行比较来评估模型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "id": "peripheral-distance",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 493 µs (started: 2021-07-20 00:04:32 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "id": "built-maintenance",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "accuracy: 0.80\n",
      "time: 25.7 ms (started: 2021-07-20 00:04:34 +08:00)\n"
     ]
    }
   ],
   "source": [
    "predictions = model(test_images)\n",
    "predictions = predictions.numpy()\n",
    "predicted_labels = np.argmax(predictions, axis=1)\n",
    "matches = predicted_labels == test_labels\n",
    "print(f\"accuracy: {matches.mean():.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "english-trader",
   "metadata": {},
   "source": [
    "全部做完！ 如您所见，“手动”完成几行 Keras 代码即可完成的工作需要大量工作。 但是因为您已经完成了这些步骤，所以您现在应该对调用 . 拥有这个 fit() 代码在幕后所做操作的低级思维模型将使您能够更好地利用 Keras API 的高级功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "portable-prerequisite",
   "metadata": {},
   "source": [
    "## 本章总结"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "primary-newcastle",
   "metadata": {},
   "source": [
    "* 张量构成了现代机器学习系统的基础。它们有dtype、rank和shape等属性。\n",
    "* 您可以通过张量运算（例如加法、张量积或元素乘法）来操作数值张量，这可以解释为编码几何变换。一般来说，深度学习中的一切都适合几何解释。\n",
    "* 深度学习模型由简单的张量运算链组成，由权重参数化，权重本身就是张量。模型的权重是存储其“知识”的地方。\n",
    "* 学习意味着为模型权重找到一组值，以最小化给定训练数据样本集及其相应目标的损失函数。\n",
    "* 学习是通过随机抽取一批数据样本及其目标，并计算模型参数相对于批次损失的梯度来进行的。然后模型参数在与梯度相反的方向移动一点（移动的幅度由学习率定义）。这称为小批量梯度下降。\n",
    "* 由于神经网络中的所有张量运算都是可微的，因此整个学习过程成为可能，因此可以应用推导链式法则来找到将当前参数和当前批次数据映射到梯度值的梯度函数。 这称为反向传播。\n",
    "* 您将在以后的章节中经常看到的两个关键概念是损失和优化器。 这些是您在开始将数据输入模型之前需要定义的两件事。\n",
    "    - 损失是您在训练期间尝试最小化的数量，因此它应该代表您尝试解决的任务是否成功的衡量标准。\n",
    "    - 优化器指定了损失梯度用于更新参数的确切方式：例如，它可以是 RMSProp 优化器、具有动量的 SGD 等。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "lightweight-aerospace",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tensorflow]",
   "language": "python",
   "name": "conda-env-tensorflow-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  },
  "toc-autonumbering": true
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
